<template>
  <div class="infinite-scroll-container" ref="containerRef">
    <!-- 下拉刷新区域 -->
    <div
      class="pull-refresh"
      :class="{ pulling: isPulling, refreshing: isRefreshing }"
    >
      <div class="refresh-indicator">
        <div v-if="isRefreshing" class="spinner"></div>
        <div v-else-if="pullDistance >= triggerDistance">释放立即刷新</div>
        <div v-else>下拉刷新</div>
      </div>
    </div>
 
    <!-- 列表内容 -->
    <div class="list-content" ref="contentRef">
      <slot
        v-for="(item, index) in list"
        :key="index"
        :item="item"
        :index="index"
      ></slot>
 
      <!-- 没有更多数据提示 -->
      <div
        v-show="noMoreData && !isLoading && !isRefreshing"
        class="no-more-data"
      >
        <slot name="no-more">没有更多数据了</slot>
      </div>
 
      <div ref="observerTarget" class="observer-target"></div>
    </div>
 
    <!-- 加载更多状态 -->
    <div v-show="isLoading" class="loading-text">
      <slot name="loading">加载更多数据...</slot>
    </div>
 
    <div v-show="hasError" class="error-text" @click="reloadData">
      <slot name="error">加载失败，请点击重试</slot>
    </div>
  </div>
</template>
 
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from "vue"
 
const props = defineProps({
  api: { type: Function, required: true },
  pageSize: { type: Number, default: 10 },
  threshold: { type: Number, default: 0.1 },
  rootMargin: { type: String, default: "200px" },
  pullRefresh: { type: Boolean, default: true },
  triggerDistance: { type: Number, default: 60 },
})
 
const emit = defineEmits([
  "load-start",
  "load-success",
  "load-error",
  "refresh-start",
  "refresh-success",
  "refresh-error",
])
 
const containerRef = ref<HTMLDivElement>(null)
const contentRef = ref<HTMLDivElement>(null)
const observerTarget = ref<HTMLDivElement>(null)
const observer = ref<IntersectionObserver | null>(null)
 
const list = ref<any[]>([])
const currentPage = ref(1)
const isLoading = ref(false)
const hasError = ref(false)
const noMoreData = ref(false)
 
// 下拉刷新状态
const isPulling = ref(false)
const isRefreshing = ref(false)
const pullDistance = ref(0)
const triggerDistance = ref(props.triggerDistance)
 
// 加载数据
const loadData = async (isInitial = false, isRefresh = false) => {
  if (isLoading.value || (isRefresh && isRefreshing.value)) return
 
  try {
    if (isRefresh) {
      isRefreshing.value = true
      emit("refresh-start")
    } else {
      isLoading.value = true
      emit("load-start", currentPage.value)
    }
 
    hasError.value = false
    const page = isRefresh ? 1 : currentPage.value
    const response = await props.api(page, props.pageSize)
    const { data = [], total = 0 } = response
 
    if (isRefresh) {
      list.value = data
      emit("refresh-success", data)
      currentPage.value = 2
      noMoreData.value = data.length < props.pageSize
    } else {
      list.value = [...list.value, ...data]
      emit("load-success", data, currentPage.value)
      currentPage.value += 1
      noMoreData.value =
        list.value.length >= total || data.length < props.pageSize
    }
  } catch (error) {
    if (isRefresh) emit("refresh-error", error)
    else emit("load-error", error)
    hasError.value = true
  } finally {
    isLoading.value = false
    isRefreshing.value = false
    pullDistance.value = 0 // 重置下拉距离
    isPulling.value = false // 重置下拉状态
 
    // 刷新完成后滚动到顶部
    if (isRefresh && contentRef.value) {
      contentRef.value.scrollTo({ top: 0, behavior: "smooth" })
    }
  }
}
 
// 初始化加载
watch(
  () => currentPage.value,
  (newPage) => {
    if (newPage === 1) loadData(true)
  },
  { immediate: true }
)
 
// IntersectionObserver 监听
onMounted(() => {
  if (!observerTarget.value) return
 
  const observerOptions = {
    rootMargin: props.rootMargin,
    threshold: props.threshold,
  }
 
  const handleIntersection = (entries: IntersectionObserverEntry[]) => {
    entries.forEach((entry) => {
      if (
        entry.isIntersecting &&
        !isLoading.value &&
        !noMoreData.value &&
        !isRefreshing.value
      ) {
        loadData()
      }
    })
  }
 
  observer.value = new IntersectionObserver(handleIntersection, observerOptions)
  observer.value.observe(observerTarget.value)
  initPullRefresh()
})
 
onUnmounted(() => {
  observer.value?.disconnect()
  removeEventListeners()
})
 
// 下拉刷新事件
let startY = 0
let isDragging = false
 
const initPullRefresh = () => {
  if (!props.pullRefresh || !contentRef.value) return
  contentRef.value.addEventListener("touchstart", handleTouchStart)
  contentRef.value.addEventListener("touchmove", handleTouchMove)
  contentRef.value.addEventListener("touchend", handleTouchEnd)
}
 
const removeEventListeners = () => {
  if (!contentRef.value) return
  contentRef.value.removeEventListener("touchstart", handleTouchStart)
  contentRef.value.removeEventListener("touchmove", handleTouchMove)
  contentRef.value.removeEventListener("touchend", handleTouchEnd)
}
 
const handleTouchStart = (e: TouchEvent) => {
  if (isRefreshing.value || isLoading.value) return // 加载/刷新中禁止触摸
  if (contentRef.value && contentRef.value.scrollTop <= 5) {
    // 允许轻微滚动误差
    startY = e.touches[0].clientY
    isDragging = true
    pullDistance.value = 0
  }
}
 
const handleTouchMove = (e: TouchEvent) => {
  if (!isDragging || isRefreshing.value || isLoading.value) return // 状态锁定
  const currentY = e.touches[0].clientY
  const diff = currentY - startY
 
  if (diff > 0) {
    e.preventDefault() // 阻止页面滚动
    const resistance = diff > 100 ? 2 : 1
    pullDistance.value = diff / resistance
    isPulling.value = true
  }
}
 
const handleTouchEnd = () => {
  if (!isDragging) return
  isDragging = false
 
  if (pullDistance.value >= triggerDistance.value) {
    loadData(false, true)
  } else {
    pullDistance.value = 0
    isPulling.value = false
  }
}
 
// 重试加载
const reloadData = () => {
  if (isRefreshing.value) return
  if (list.value.length === 0) {
    currentPage.value = 1
    list.value = []
    noMoreData.value = false
    loadData(true)
  } else {
    currentPage.value = Math.max(1, currentPage.value - 1)
    loadData()
  }
}
</script>
 
<style scoped>
.infinite-scroll-container {
  position: relative;
  overflow: hidden;
  height: 80vh;
}
 
.pull-refresh {
  position: absolute;
  top: -4.1667vw;
  left: 0;
  width: 100%;
  height: 12vw;
  display: flex;
  justify-content: center;
  align-items: center;
  transition: all 0.3s ease; /* 统一过渡属性 */
  opacity: 1;
  z-index: 10;
}
 
/* 下拉状态 */
.pull-refresh.pulling {
  transform: translateY(4.1667vw);
  opacity: 1;
}
 
/* 刷新状态 */
.pull-refresh.refreshing {
  transform: translateY(4.1667vw);
  opacity: 0.8; /* 刷新中半透明 */
}
 
/* 隐藏状态（刷新完成后） */
.pull-refresh:not(.pulling):not(.refreshing) {
  transform: translateY(-100%); /* 向上移出视口 */
  opacity: 0;
}
 
.refresh-indicator {
  display: flex;
  align-items: center;
  font-size: 3vw;
  color: #666;
}
 
.spinner {
  width: 4vw;
  height: 4vw;
  border: 0.9vw solid #ccc;
  border-radius: 50%;
  border-top-color: #333;
  animation: spin 1s linear infinite;
  margin-right: 1vw;
}
 
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
 
.list-content {
  height: 100%;
  overflow-y: auto;
  position: relative;
}
 
.observer-target {
  height: 10vw;
  visibility: hidden;
}
 
.loading-text,
.error-text,
.no-more-data {
  padding: 4vw;
  text-align: center;
  font-size: 3vw;
}
 
.loading-text {
  color: #666;
}
.error-text {
  color: #ff4d4f;
  cursor: pointer;
}
.no-more-data {
  color: #999;
}
 
.error-text:hover {
  text-decoration: underline;
}
</style>